Turtle Island(註1)為小弟編寫的套件,其目的是希望能讓使用者專注地編寫expr,而非處理相關的boilerplate code。
今天我將分享Turtle Island的核心精神及其特點(註2)。
本日大綱如下:
pl.DataFrame
級別操作pl.Expr
級別操作import polars as pl
df = pl.DataFrame(
{
"col1": [1, 2, 3, 4, 5],
"col2": [6, 7, 8, 9, 10],
"col3": [11, 12, 13, 14, 15],
}
)
shape: (5, 3)
┌──────┬──────┬──────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞══════╪══════╪══════╡
│ 1 ┆ 6 ┆ 11 │
│ 2 ┆ 7 ┆ 12 │
│ 3 ┆ 8 ┆ 13 │
│ 4 ┆ 9 ┆ 14 │
│ 5 ┆ 10 ┆ 15 │
└──────┴──────┴──────┘
df2 = pl.DataFrame(
{
"x": [[1, 2, 3], [4, 5, 6]],
"y": [[7, 8, 9], [10, 11, 12]],
}
)
shape: (2, 2)
┌───────────┬──────────────┐
│ x ┆ y │
│ --- ┆ --- │
│ list[i64] ┆ list[i64] │
╞═══════════╪══════════════╡
│ [1, 2, 3] ┆ [7, 8, 9] │
│ [4, 5, 6] ┆ [10, 11, 12] │
└───────────┴──────────────┘
目前Turtle Island尚未上架PyPI,使用者必須由GitHub repo安裝,例如:
pip install git+https://github.com/jrycw/turtle-island.git
或使用uv
:
uv add git+https://github.com/jrycw/turtle-island.git
為避免命名衝突,建議使用下列方式引入Turtle Island:
import turtle_island as ti
Turtle Island的核心思維是,盡量使用pl.Expr
而非pl.Dataframe
級別的函數進行操作。
舉例來說,針對下面要求,使用df
比較pl.DataFrame
及pl.Expr
之操作:
pl.DataFrame
級別操作pl.DataFrame
級別的操作,必須依次進行下面兩個步驟:
.with_columns()
context中,使用when-then-otherwise
編寫mod運算的取值邏輯。(
df.with_row_index().with_columns(
pl.when(pl.col("index").mod(2).eq(0))
.then(pl.col("col1", "col2"))
.otherwise("col3"),
)
)
shape: (5, 4)
┌───────┬──────┬──────┬──────┐
│ index ┆ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- ┆ --- │
│ u32 ┆ i64 ┆ i64 ┆ i64 │
╞═══════╪══════╪══════╪══════╡
│ 0 ┆ 1 ┆ 6 ┆ 11 │
│ 1 ┆ 12 ┆ 12 ┆ 12 │
│ 2 ┆ 3 ┆ 8 ┆ 13 │
│ 3 ┆ 14 ┆ 14 ┆ 14 │
│ 4 ┆ 5 ┆ 10 ┆ 15 │
└───────┴──────┴──────┴──────┘
「"index"」列並不是我們感興趣的部份,但卻必須先新增它,才能進行後續mod運算。
pl.Expr
級別操作ti.is_every_nth_row()會依據輸入值判斷其是否為第n行的倍數,返回True
或False
,並可直接於context中使用,例如:
df.with_columns(
ti.case_when(
case_list=[(ti.is_every_nth_row(2), pl.col("col1", "col2"))],
otherwise="col3",
)
)
shape: (5, 3)
┌──────┬──────┬──────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞══════╪══════╪══════╡
│ 1 ┆ 6 ┆ 11 │
│ 12 ┆ 12 ┆ 12 │
│ 3 ┆ 8 ┆ 13 │
│ 14 ┆ 14 ┆ 14 │
│ 5 ┆ 10 ┆ 15 │
└──────┴──────┴──────┘
其中,ti.case_when()是Turtle Island針對when-then-otherwise
所提供的語法糖。
Turtle Island在不新增「"index"」列的情況下,完成所需運算。
由於Turtle Island提供的函數大多數會返回expr,這將使得這些函數也能在.list.eval()
中使用。
以ti.cycle()為例,可以將所選expr的元素往下一行,並將最後一個元素「擠」至最前,例如:
df.select(df.select(ti.cycle(pl.all())))
shape: (5, 3)
┌──────┬──────┬──────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞══════╪══════╪══════╡
│ 5 ┆ 10 ┆ 15 │
│ 1 ┆ 6 ┆ 11 │
│ 2 ┆ 7 ┆ 12 │
│ 3 ┆ 8 ┆ 13 │
│ 4 ┆ 9 ┆ 14 │
└──────┴──────┴──────┘
如果在.list.eval()
中執行,也可以得到如預期般向下「擠」的結果,例如:
df2.with_columns(pl.all().list.eval(ti.cycle(pl.element(), 2)))
shape: (2, 2)
┌───────────┬──────────────┐
│ x ┆ y │
│ --- ┆ --- │
│ list[i64] ┆ list[i64] │
╞═══════════╪══════════════╡
│ [2, 3, 1] ┆ [8, 9, 7] │
│ [5, 6, 4] ┆ [11, 12, 10] │
└───────────┴──────────────┘
此處展示了ti.cycle()
向下「擠」兩個元素的結果。
註1:Turtle Island雖是通用型的Polars expr套件,但特別適合搭配Great Tables使用,是開發此套件的初衷。
註2:本日內容大多摘自Turtle Island的intro文件。
註3:由於pl.DataFrame.with_row_index()
由0作為索引值開頭,所以在思考奇數偶數mod運算的時候,需要隔外小心。另一個做法是寫為pl.DataFrame.with_row_index(offset=1)
,這麼一來索引值將由1開始,可能會有助於思考。
個人部落格文章:Notes on Turtle Island。